﻿/*
 * Copyright (C) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

class PlayGameServicesManager : AndroidJavaProxy {
    private const string BaseGameUtilsPkg = "com.google.example.games.basegameutils";
    private const string SupportLibPkg = "com.google.example.games.unitysupportlib";
    private const string GameHelperClass = BaseGameUtilsPkg + ".GameHelper";
    private const string GameHelperListenerClass = BaseGameUtilsPkg + ".GameHelper$GameHelperListener";
    private const string OnAchievementsLoadedListenerClass = "com.google.android.gms.games.achievement.OnAchievementsLoadedListener";

    private static PlayGameServicesManager instance = null;
    internal static PlayGameServicesManager Instance {
        get {
            if (instance == null) {
                LogD("Instantiating PlayGameServicesManager singleton.");
                instance = new PlayGameServicesManager();
            }
            return instance;
        }
    }

    private AndroidJavaObject mGameHelper = null;
    private bool mSetupDone = false;

    // should we print debug logs?
    private static bool mDebugLogs = false;

    // should we attempt a user-initiated sign after setup?
    private bool mPendingUserInitiatedSignIn = false;

    // callback that we should invoke after user-initiated sign-in
    private System.Action<bool> mSignInCallback = null;

    // status of sign in
    internal enum SignInStatus { NotSignedIn, InProgress, SignedIn };
    private SignInStatus mSignInStatus = SignInStatus.NotSignedIn;
    internal bool IsSignedIn { get { return mSignInStatus == SignInStatus.SignedIn; } }

    // spool of jobs we have to execute once we're connected
    private List<System.Action<AndroidJavaObject,AndroidJavaObject>> mJobSpool
            = new List<System.Action<AndroidJavaObject,AndroidJavaObject>>();

    internal PlayGameServicesManager() : base(GameHelperListenerClass) {
    }

    internal AndroidJavaObject GetActivity() {
        AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        if (jc == null) {
            throw new System.Exception("Could not get class com.unity3d.player.UnityPlayer.");
        }
        AndroidJavaObject activity = jc.GetStatic<AndroidJavaObject>("currentActivity");
        if (activity == null) {
            throw new System.Exception("Could not get class current activity from UnityPlayer.");
        }
        return activity;
    }

    internal bool SetupAndSignIn(System.Action<bool> callback) {
        if (mSetupDone) {
            throw new System.InvalidOperationException("PlayGameServicesManager: was already set up.");
        }

        // get game Activity
        LogD("Starting PlayGameServicesPlatform setup. Trying to get Activity...");
        AndroidJavaObject activity = GetActivity();
        LogD("Activity is " + activity);

        // do some sanity checks on the Activity
        string title = activity.Call<string>("getTitle");
        string className = activity.Call<string>("getLocalClassName");
        LogD("Activity title " + title);
        LogD("Local class name: " + activity.Call<string>("getLocalClassName"));

        if (className.StartsWith("com.unity3d.player.")) {
            LogW("Activity's class is " + className + ": looks like a standard Unity Activity class.");
            LogW("This class will likely not work with PlayGameServicesPlatform (you will probably see a crash");
            LogW("soon). Instead, use a custom Activity class that implements setLifecycleListener().");
        }

        // create the GameHelper object
        LogD("Creating GameHelper");
        mGameHelper = new AndroidJavaObject(GameHelperClass, activity);

        // enable debug logs
        if (Debug.isDebugBuild) {
            mDebugLogs = true;
            LogD("Enabling debug logging on GameHelper.");
            mGameHelper.Call("enableDebugLog", true, "GameHelper");
        }

        // create and set up the lifecycle listener so we get notified of Activity
        // lifecycle events that we need
        LogD("Creating life cycle listener...");
        PlayGameServicesLifecycleListener l = new PlayGameServicesLifecycleListener(mGameHelper);
        LogD("Setting up life cycle listener...");
        activity.Call("setLifecycleListener", l);
        LogD("Life cycle listener is set!");

        // set up our state variables
        mPendingUserInitiatedSignIn = true;
        mSetupDone = true;
        mSignInCallback = callback;
        mSignInStatus = SignInStatus.InProgress;

        // perform setup on the GameHelper object.
        LogD("Setting up GameHelper");
        runOnUiThread(() => {
            mGameHelper.Call("setup", this);
            mGameHelper.Call("onStart");
        });
        mGameHelper.Call("invokeSetup", this, true);

        LogD("PlayGameServicesPlatform has been prepared successfully.");
        return true;

        // Now, what should happen is that Play Games will attempt to sign in
        // automatically. Then, we will get a callback (onSignInSucceeded or onSignInFailed)
        // which is going to notify us either that sign-in was successful or that it failed. If
        // it failed, we can still attempt manual (user-initiated) sign-in by calling
        // GameHelper.beginUserInitiatedSignIn().
    }

    private void runOnUiThread(System.Action action) {
        AndroidJavaObject activity = GetActivity();
        activity.Call("runOnUiThread", new AndroidJavaRunnable(action));
    }

    private void AssertSetupDone(string method) {
        if (!mSetupDone) {
            throw new System.InvalidOperationException("PlayGameServicesManager." + method + "(): set up not done!");
        }
    }
    private void AssertStatus(string method, SignInStatus requiredStatus) {
        if (mSignInStatus == requiredStatus) return;
        throw new System.InvalidOperationException("PlayGameServicesManager." + method + "() called in invalid " +
            "sign-in state. Required: " + requiredStatus + ". Actual: " + mSignInStatus);
    }

    internal void DoUserInitiatedSignIn(System.Action<bool> callback) {
        AssertSetupDone("DoUserInitiatedSignIn");
        AssertStatus("DoUserInitiatedSignIn", SignInStatus.NotSignedIn);
        LogD("Beginning user-initiated sign-in flow.");
        mSignInCallback = callback;
        mGameHelper.Call("beginUserInitiatedSignIn");
    }

    internal bool IsSetupDone() {
        return mSetupDone;
    }

    internal static void LogD(string msg) {
        if (mDebugLogs) {
            Debug.Log("PlayGameServicesPlatform: " + msg);
        }
    }

    internal static void LogW(string msg) {
        Debug.LogWarning("PlayGameServicesPlatform: WARNING: " + msg);
    }

    internal static void LogE(string msg) {
        Debug.LogError("PlayGameServicesPlatform: ERROR: " + msg);
    }

    // method of GameHelper$GameHelperListener
    void onSignInFailed() {
        mSignInStatus = SignInStatus.NotSignedIn;
        LogD("Callback: onSignInFailed.");
        if (mPendingUserInitiatedSignIn) {
            // auto sign in has failed; attempt user-initiated sign-in
            mPendingUserInitiatedSignIn = false;
            LogD("User-initiated sign-in pending. Starting.");
            DoUserInitiatedSignIn(mSignInCallback);
        }
        else if (mSignInCallback != null) {
            LogD("Invoking sign-in callback with FAILURE.");
            mSignInCallback.Invoke(false);
            mSignInCallback = null;
        }
    }

    // method of GameHelper$GameHelperListener
    void onSignInSucceeded() {
        LogD("Callback: onSignInSucceeded");
        mSignInStatus = SignInStatus.SignedIn;
        if (mPendingUserInitiatedSignIn) mPendingUserInitiatedSignIn = false;
        if (mSignInCallback != null) {
            LogD("Invoking sign-in callback with SUCCESS.");
            mSignInCallback.Invoke(true);
            mSignInCallback = null;
        }

        // process any pending jobs
        ProcessJobQueue();
    }

    // method of GameHelper$GameHelperListener
    void onInvitedToRoom(string invId) {
        LogD("Callback: onInvitedToRoom: " + invId);
    }

    internal void QueueJob(System.Action<AndroidJavaObject,AndroidJavaObject> job) {
        LogD("New job spooled.");
        mJobSpool.Add(job);
        ProcessJobQueue();
    }

    void ProcessJobQueue() {
        if (mJobSpool.Count <= 0) return; // nothing to do
        if (mSignInStatus != SignInStatus.SignedIn) {
            // can't do this now -- will do when we get onSignInSucceeded()
            LogD("Can't process job queue now -- not signed in. Waiting until later.");
            return;
        }
        LogD("Processing job queue: " + mJobSpool.Count + " job(s).");

        // move jobs to another queue, because it will be processed from a
        // different thread.
        List<System.Action<AndroidJavaObject,AndroidJavaObject>> queue = mJobSpool;
        mJobSpool = new List<System.Action<AndroidJavaObject,AndroidJavaObject>>();

        AndroidJavaObject activity = GetActivity();
        AndroidJavaObject gamesClient = GetGamesClient();

        // run jobs on UI thread
        runOnUiThread(() => {
            foreach (System.Action<AndroidJavaObject,AndroidJavaObject> job in queue) {
                job.Invoke(activity, gamesClient);
            }
        });
    }

    internal AndroidJavaObject GetGamesClient() {
        AssertSetupDone("GetGamesClient");
        AssertStatus("GetGamesClient", SignInStatus.SignedIn);
        AndroidJavaObject gamesClient = mGameHelper.Call<AndroidJavaObject>("getGamesClient");
        if (gamesClient == null) throw new System.Exception("GetGamesClient(): GamesClient is null!");
        return gamesClient;
    }
}
